package com.chenwc.amazons3;

import com.chenwc.common.util.Util;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.waiters.WaiterResponse;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.services.s3.waiters.S3Waiter;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.*;
import software.amazon.awssdk.transfer.s3.progress.TransferProgress;
import software.amazon.awssdk.transfer.s3.progress.TransferProgressSnapshot;

import java.io.*;
import java.net.URI;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * AWS 操作工具类
 *
 * @author chenwc
 */
public class AwsUtil {

    private static final Logger log = LoggerFactory.getLogger(AwsUtil.class);

    /**
     * 文件分块大小,10M
     */
    private static final Integer CHUNK_FILE_SIZE = 10 * 1024 * 1024;

    /**
     * 文件分块阈值,10M
     */
    private static final Integer CHUNK_FILE_SIZE_THRESHOLD = 10 * 1024 * 1024;

    private static final Double targetThroughputInGbps = 20.0;

    /**
     * MinioClient
     */
    private static S3Client s3Client = null;

    private static S3AsyncClient s3AsyncClient = null;

    private static String endpoint;
    private static String accessKey;
    private static String secretKey;
    private static Region region;

    /**
     * 每次查询的最大key数量
     */
    private static final Integer MAX_KEY_PER_PAGE = 1000;

    /**
     * 线程安全的 DecimalFormat
     */
    private static final ThreadLocal<DecimalFormat> THREAD_LOCAL_DECIMAL_FORMAT = new ThreadLocal<>();

    /**
     * 获取 DecimalFormat
     *
     * @return DecimalFormat
     */
    public static DecimalFormat getDecimalFormat() {
        DecimalFormat df = THREAD_LOCAL_DECIMAL_FORMAT.get();
        if (df == null) {
            df = new DecimalFormat("#.00");
            THREAD_LOCAL_DECIMAL_FORMAT.set(df);
        }
        return df;
    }

    /**
     * 创建s3客户端
     *
     * @param userEndpoint  用户自定义endpoint
     * @param userAccessKey 用户自定义accessKey
     * @param userSecretKey 用户自定义secretKey
     * @param userRegion    用户自定义region
     */
    public static void getS3Client(String userEndpoint, String userAccessKey,
                                   String userSecretKey, Region userRegion) {
        endpoint = userEndpoint;
        accessKey = userAccessKey;
        secretKey = userSecretKey;
        region = userRegion;
    }

    /**
     * 创建s3客户端
     */
    public static void getS3Client() {
        if (null == s3Client) {
            AwsBasicCredentials awsCreds = AwsBasicCredentials.create(
                    accessKey,
                    secretKey);
            s3Client = S3Client.builder()
                    .region(region)
                    .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
                    .endpointOverride(URI.create(endpoint))
                    //强制使用地址类型风格的url，如果设置为false会使用域名类型
                    .forcePathStyle(true)
                    .build();
        }

    }

    /**
     * 创建s3异步客户端
     */
    public static void getS3AsyncClient() {
        if (null == s3AsyncClient) {
            AwsBasicCredentials awsCreds = AwsBasicCredentials.create(
                    accessKey,
                    secretKey);
            s3AsyncClient = S3AsyncClient.crtBuilder()
                    .region(region)
                    .endpointOverride(URI.create(endpoint))
                    .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
                    .targetThroughputInGbps(targetThroughputInGbps)
                    .minimumPartSizeInBytes(Long.valueOf(CHUNK_FILE_SIZE_THRESHOLD))
                    .build();
        }
    }

    /**
     * 关闭s3异步客户端
     */
    public static void closeAsync() {
        if (null != s3AsyncClient) {
            try {
                s3AsyncClient.close();
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
    }

    /**
     * 关闭s3客户端
     */
    public static void close() {
        if (null != s3Client) {
            try {
                s3Client.close();
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
    }

    /**
     * 使用S3Waiter对象创建存储桶
     *
     * @param bucketName 桶名
     */
    public static void createBucket(String bucketName) {

        try {
            getS3Client();
            if (bucketExists(bucketName)) {
                log.info("桶名：{} 已存在，不需要再创建！", bucketName);
                return;
            }
            //创建 S3Waiter 对象
            S3Waiter s3Waiter = s3Client.waiter();
            CreateBucketRequest bucketRequest = CreateBucketRequest.builder()
                    .bucket(bucketName)
                    .build();
            //创建请求
            s3Client.createBucket(bucketRequest);
            HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder()
                    .bucket(bucketName)
                    .build();

            // 等待创建存储桶并打印出响应。
            WaiterResponse<HeadBucketResponse> waiterResponse = s3Waiter.waitUntilBucketExists(bucketRequestWait);
            boolean isExists = waiterResponse.matched().response().isPresent();
            log.info("存储桶已存在: {}", isExists);
            log.info(bucketName + " 存储桶创建完成");
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 列出服务器上所有存储桶
     */
    public static void listBucket() {
        getS3Client();
        // List buckets
        ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
        ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest);
        List<Bucket> list = listBucketsResponse.buckets();
        for (Bucket b : list) {
            log.info("桶名: " + b.name());
        }
    }

    /**
     * 判断桶名是否已存在
     *
     * @param bucketName 桶名
     * @return 已存在返回true，不存在返回false
     */
    public static boolean bucketExists(String bucketName) {
        getS3Client();
        // List buckets
        ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
        ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest);
        List<Bucket> list = listBucketsResponse.buckets();
        for (Bucket b : list) {
            if (b.name().equals(bucketName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 清空存储桶内所有对象并删除存储桶
     *
     * @param bucketName 桶名
     */
    public static void deleteObjectsInBucket(String bucketName) {
        try {
            getS3Client();
            String versionStatus = getBucketVersioningStatus(bucketName);
            // 判断是否开启版本控制
            boolean isVersioningEnabled = StringUtils.isNotEmpty(versionStatus) && "Enabled".equals(versionStatus);
            log.info("是否开启版本控制: {}", isVersioningEnabled);
            // 要删除存储桶，必须首先删除存储桶中的所有对象。
            if (isVersioningEnabled) {
                ListObjectVersionsRequest listObjectVersionsRequest = ListObjectVersionsRequest.builder()
                        .bucket(bucketName)
                        .maxKeys(MAX_KEY_PER_PAGE)
                        .build();
                ListObjectVersionsResponse listObjectVersionsResponse = s3Client.listObjectVersions(listObjectVersionsRequest);
                List<ObjectVersion> objectVersions = listObjectVersionsResponse.versions();
                // 删除对象
                deleteObjectVersion(bucketName, objectVersions);
                // 如果有更多的对象版本，可以使用isTruncated方法和getNextMarker方法进行迭代
                while (listObjectVersionsResponse.isTruncated()) {
                    String nextMarker = listObjectVersionsResponse.nextKeyMarker();
                    log.info("继续删除对象: {}", nextMarker);
                    listObjectVersionsRequest = ListObjectVersionsRequest.builder()
                            .bucket(bucketName)
                            .maxKeys(MAX_KEY_PER_PAGE)
                            .keyMarker(nextMarker)
                            .versionIdMarker(nextMarker)
                            .build();
                    listObjectVersionsResponse = s3Client.listObjectVersions(listObjectVersionsRequest);
                    objectVersions = listObjectVersionsResponse.versions();
                    deleteObjectVersion(bucketName, objectVersions);
                }
            } else {
                ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder()
                        .bucket(bucketName)
                        .maxKeys(MAX_KEY_PER_PAGE)
                        .build();
                //删除存储桶中的所有对象。
                boolean done = false;
                while (!done) {
                    ListObjectsV2Response listObjResponse = s3Client.listObjectsV2(listObjectsV2Request);
                    for (S3Object content : listObjResponse.contents()) {
                        deleteObject(bucketName, content.key(), "");
                    }
                    //判断是否还有下一页
                    if (listObjResponse.nextContinuationToken() == null) {
                        done = true;
                    }
                    //获取下一页
                    listObjectsV2Request = listObjectsV2Request.toBuilder()
                            .continuationToken(listObjResponse.nextContinuationToken())
                            .build();
                }
            }

            //删除空存储桶
            DeleteBucketRequest deleteBucketRequest = DeleteBucketRequest.builder()
                    .bucket(bucketName)
                    .build();
            s3Client.deleteBucket(deleteBucketRequest);
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 删除对象
     *
     * @param bucketName     桶名
     * @param objectVersions 对象版本列表
     */
    private static void deleteObjectVersion(String bucketName, List<ObjectVersion> objectVersions) {
        log.info("删除对象数量: {}", objectVersions.size());
        if (!objectVersions.isEmpty()) {
            for (ObjectVersion objectVersion : objectVersions) {
                DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
                        .bucket(bucketName)
                        .key(objectVersion.key())
                        .versionId(objectVersion.versionId())
                        .build();
                s3Client.deleteObject(deleteObjectRequest);
                log.info("存储桶: {} 中 key: {} 版本: {} 的文件删除成功！", bucketName, objectVersion.key(), objectVersion.versionId());
            }
        }
    }

    /**
     * 上传对象
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param file       上传文件对象
     * @return 是否上传成功，是返回true，否返回false
     */
    public static boolean uploadObject(String bucketName, String key, File file) {
        log.info("--------------------------开始上传对象: {} ---------------------------------", key);
        getS3Client();

        log.info("在存储桶上传对象: {}", bucketName);
        log.info("上传对象 Key : {}", key);
        printObjectSize(file.length());
        PutObjectRequest objectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();

        PutObjectResponse putObjectResponse = s3Client.putObject(objectRequest, RequestBody.fromFile(file));
        boolean isUploadSuccessful = putObjectResponse.sdkHttpResponse().isSuccessful();
        log.info("isUploadSuccessful: {}", isUploadSuccessful);
        log.info("uploadObject statusCode: {}", putObjectResponse.sdkHttpResponse().statusCode());
        log.info("uploadObject statusText: {}", putObjectResponse.sdkHttpResponse().statusText());
        log.info("--------------------------上传对象: {} 完成---------------------------------", key);
        return isUploadSuccessful;
    }

    /**
     * 分片上传对象
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param file       上传文件对象
     * @return 是否上传成功，是返回true，否返回false
     */
    public static boolean multipartUploadObject(String bucketName, String key, File file) {
        log.info("--------------------------分片上传对象开始: {} ---------------------------------", key);
        getS3Client();
        // 首先创建多部分上传并获取上传id
        log.info("在存储桶分片上传对象: {}", bucketName);
        log.info("分片上传对象 Key: {}", key);
        printObjectSize(file.length());
        CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();
        CreateMultipartUploadResponse response = s3Client.createMultipartUpload(createMultipartUploadRequest);
        String uploadId = response.uploadId();
        log.info("uploadId: " + uploadId);

        //文件分块数量
        int fileNum = (int) Math.ceil(file.length() * 1.0 / CHUNK_FILE_SIZE);
        List<CompletedPart> completedPartList = new ArrayList<>();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            long available = file.length();
            log.info("available: {}", available);
            if (available <= 0) {
                log.info("待读取文件为空文件，读取文件失败");
                return false;
            }
            byte[] result = new byte[(int) available];
            //读取总的字节数
            int readBytesNum = fis.read(result);
            for (int i = 1; i <= fileNum; i++) {
                log.info("正在上传第：{} 个文件分片....", i);
                byte[] upload;
                if (i < fileNum) {
                    upload = new byte[CHUNK_FILE_SIZE];
                    //src 源数组，srcPos 从源数组哪个位置开始拷贝，dest 目标数组，destPos 从目标数组哪个位置开始复制，length 复制多少长度
                    int startIndex = (i - 1) * CHUNK_FILE_SIZE;
                    log.debug("startIndex: {}", startIndex);
                    log.debug("endIndex: {}", startIndex + CHUNK_FILE_SIZE);
                    System.arraycopy(result, startIndex, upload, 0, CHUNK_FILE_SIZE);
                }
                //最后一块
                else {
                    upload = new byte[(int) (file.length() - CHUNK_FILE_SIZE * (i - 1))];
                    int startIndex = (i - 1) * CHUNK_FILE_SIZE;
                    int length = (int) (file.length() - CHUNK_FILE_SIZE * (i - 1));
                    log.debug("startIndex: {}", startIndex);
                    log.debug("endIndex: {}", startIndex + length);
                    System.arraycopy(result, startIndex, upload, 0, length);
                }
                //上载对象的所有不同部分
                UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
                        .bucket(bucketName)
                        .key(key)
                        .uploadId(uploadId)
                        .partNumber(i).build();
                String etag = s3Client.uploadPart(uploadPartRequest, RequestBody.fromBytes(upload)).eTag();
                CompletedPart part = CompletedPart.builder().partNumber(i).eTag(etag).build();
                completedPartList.add(part);
                log.info("第：{} 个文件分片上传完成....", i);
            }
        } catch (Exception e) {
            log.error("Error occurred: ", e);
            return false;
        } finally {
            try {
                if (null != fis) {
                    fis.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }

        // 最后调用completeMultipartUpload操作，告诉S3合并所有上传的部分并完成多部分操作。
        CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
                .parts(completedPartList)
                .build();
        CompleteMultipartUploadRequest completeMultipartUploadRequest =
                CompleteMultipartUploadRequest.builder()
                        .bucket(bucketName)
                        .key(key)
                        .uploadId(uploadId)
                        .multipartUpload(completedMultipartUpload)
                        .build();
        CompleteMultipartUploadResponse completeMultipartUploadResponse = s3Client.completeMultipartUpload(completeMultipartUploadRequest);
        boolean isUploadSuccessful = completeMultipartUploadResponse.sdkHttpResponse().isSuccessful();
        log.info("isMultipartUploadSuccessful: {}", isUploadSuccessful);
        log.info("MultipartUploadObject statusCode: {}", completeMultipartUploadResponse.sdkHttpResponse().statusCode());
        log.info("MultipartUploadObject statusText: {}", completeMultipartUploadResponse.sdkHttpResponse().statusText());
        log.info("--------------------------上传对象: {} 完成---------------------------------", key);
        log.info("--------------------------分片上传对象结束: {} ---------------------------------", key);
        return isUploadSuccessful;
    }

    /**
     * 下载对象
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param savePath   待写入本地文件全路径+文件名
     * @return 是否下载成功，是返回true，否返回false
     */
    public static boolean downloadObject(String bucketName, String key, String savePath) {
        log.info("--------------------------开始下载对象: {} ---------------------------------", key);
        getS3Client();
        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();

        ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(getObjectRequest);
        InputStream inputStream = responseBytes.asInputStream();
        Util.convertInputStreamToLocalFile(inputStream, savePath);
        GetObjectResponse getObjectResponse = responseBytes.response();
        boolean isDownloadSuccessful = getObjectResponse.sdkHttpResponse().isSuccessful();
        log.info("isDownloadSuccessful: {}", isDownloadSuccessful);
        log.info("downloadObject statusCode: {}", getObjectResponse.sdkHttpResponse().statusCode());
        log.info("downloadObject statusText: {}", getObjectResponse.sdkHttpResponse().statusText());
        log.info("--------------------------下载对象: {} 完成---------------------------------", key);
        return isDownloadSuccessful;
    }

    /**
     * 下载对象
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param savePath   待写入本地文件全路径+文件名
     * @return 是否下载成功，是返回true，否返回false
     */
    public static boolean downloadObjectByBytes(String bucketName, String key, String savePath) {
        log.info("--------------------------开始下载对象: {} ---------------------------------", key);
        getS3Client();
        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();
        ResponseBytes<GetObjectResponse> objectBytes = s3Client.getObjectAsBytes(getObjectRequest);
        byte[] data = objectBytes.asByteArray();

        // Write the data to a local file.
        File myFile = new File(savePath);
        OutputStream os = null;
        try {
            os = Files.newOutputStream(myFile.toPath());
            os.write(data);
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (null != os) {
                    os.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        GetObjectResponse getObjectResponse = objectBytes.response();
        boolean isDownloadSuccessful = getObjectResponse.sdkHttpResponse().isSuccessful();
        log.info("isDownloadSuccessful: {}", isDownloadSuccessful);
        log.info("downloadObject statusCode: {}", getObjectResponse.sdkHttpResponse().statusCode());
        log.info("downloadObject statusText: {}", getObjectResponse.sdkHttpResponse().statusText());
        log.info("--------------------------下载对象: {} 完成---------------------------------", key);
        return isDownloadSuccessful;
    }

    /**
     * 删除对象
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param versionId  对象版本号，为空时，删除最新版本
     * @return 是否删除成功，是返回true，否返回false
     */
    public static boolean deleteObject(String bucketName, String key, String versionId) {
        log.info("--------------------------开始删除对象: {} ---------------------------------", key);
        getS3Client();
        DeleteObjectRequest deleteObjectRequest;
        if (StringUtils.isEmpty(versionId)) {
            deleteObjectRequest = DeleteObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .versionId(versionId)
                    .build();
        } else {
            deleteObjectRequest = DeleteObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .build();
        }
        DeleteObjectResponse response = s3Client.deleteObject(deleteObjectRequest);
        boolean isDeleteSuccessful = response.sdkHttpResponse().isSuccessful();
        log.info("isDeleteSuccessful: {}", isDeleteSuccessful);
        log.info("deleteObject statusCode: {}", response.sdkHttpResponse().statusCode());
        log.info("deleteObject statusText: {}", response.sdkHttpResponse().statusText());
        log.info("--------------------------删除对象: {} 完成---------------------------------", key);
        if (isDeleteSuccessful) {
            log.info("存储桶: {} 中 key: {} 版本: {} 的文件删除成功！", bucketName, key, versionId);
            return true;
        } else {
            log.info("存储桶: {} 中 key: {} 版本: {} 的文件删除失败！", bucketName, key, versionId);
            return false;
        }
    }

    /**
     * 获取对象标签
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @return 对象标签
     */
    public static List<Tag> getObjectTags(String bucketName, String key) {
        try {
            getS3Client();
            GetObjectTaggingRequest getTaggingRequest = GetObjectTaggingRequest
                    .builder()
                    .key(key)
                    .bucket(bucketName)
                    .build();
            GetObjectTaggingResponse tags = s3Client.getObjectTagging(getTaggingRequest);
            return tags.tagSet();
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
        return null;
    }

    /**
     * 获取对象URL
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @return 对象URL
     */
    public static String getObjectUrl(String bucketName, String key) {
        try {
            getS3Client();
            GetUrlRequest request = GetUrlRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .build();

            URL url = s3Client.utilities().getUrl(request);
            return url.toString();
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
        return null;
    }

    /**
     * 获取对象预签名URL，默认过期时间为1小时
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @return 对象预签名URL
     */
    public static String getObjectPresignedUrl(String bucketName, String key) {
        getS3Client();
        return getObjectPresignedUrl(bucketName, key, 1, TimeUnit.HOURS);
    }

    /**
     * 获取对象预签名URL
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param expireTime 预签名URL过期时间
     * @param timeUnit   预签名URL过期时间单位
     * @return 对象预签名URL
     */
    public static String getObjectPresignedUrl(String bucketName, String key, long expireTime, TimeUnit timeUnit) {
        S3Presigner presigner = null;
        try {
            AwsBasicCredentials awsCreds = AwsBasicCredentials.create(
                    accessKey,
                    secretKey);
            presigner = S3Presigner.builder()
                    .region(region)
                    .endpointOverride(URI.create(endpoint))
                    .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
                    .build();
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .build();
            Duration duration;
            switch (timeUnit) {
                case MILLISECONDS:
                    duration = Duration.ofMillis(expireTime);
                    break;
                case SECONDS:
                    duration = Duration.ofSeconds(expireTime);
                    break;
                case MINUTES:
                    duration = Duration.ofMinutes(expireTime);
                    break;
                case HOURS:
                    duration = Duration.ofHours(expireTime);
                    break;
                case DAYS:
                    duration = Duration.ofDays(expireTime);
                    break;
                default:
                    throw new RuntimeException("时间单位不对，取值范围是：MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS");
            }
            GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
                    .signatureDuration(duration)
                    .getObjectRequest(getObjectRequest)
                    .build();
            PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest);
            return presignedGetObjectRequest.url().toString();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (presigner != null) {
                    presigner.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        return null;
    }

    /**
     * 列出存储桶内所有对象
     *
     * @param bucketName 桶名
     */
    public static void listBucketObjects(String bucketName) {
        try {
            getS3Client();
            ListObjectsRequest listObjects = ListObjectsRequest
                    .builder()
                    .bucket(bucketName)
                    .build();
            ListObjectsResponse res = s3Client.listObjects(listObjects);
            List<S3Object> objects = res.contents();
            for (S3Object myValue : objects) {
                log.info("对象 Key： " + myValue.key());
                printObjectSize(myValue.size());
                log.info("对象拥有者： " + myValue.owner());
            }
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 查找在同一个桶内有没有相同key的文件
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @return 存在返回true，不存在返回false
     */
    public static boolean isObjectExist(String bucketName, String key) {
        try {
            getS3Client();
            ListObjectsRequest listObjects = ListObjectsRequest
                    .builder()
                    .bucket(bucketName)
                    .build();
            ListObjectsResponse res = s3Client.listObjects(listObjects);
            List<S3Object> objects = res.contents();
            for (S3Object myValue : objects) {
                if (myValue.key().equals(key)) {
                    return true;
                }
            }
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
        return false;
    }

    /**
     * 复制对象
     *
     * @param srcBucket         源桶名
     * @param srcKey            源对象
     * @param destinationBucket 目标桶名
     * @param destinationKey    目标对象
     */
    public static void copyBucketObject(String srcBucket, String srcKey,
                                        String destinationBucket, String destinationKey) {
        getS3Client();
        CopyObjectRequest copyReq = CopyObjectRequest.builder()
                .sourceBucket(srcBucket)
                .sourceKey(srcKey)
                .destinationBucket(destinationBucket)
                .destinationKey(destinationKey)
                .build();
        try {
            CopyObjectResponse copyRes = s3Client.copyObject(copyReq);
            log.info("对象复制结果：{}", copyRes.copyObjectResult().toString());
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 打印对象大小
     *
     * @param myValueSize 对象大小
     */
    private static void printObjectSize(Long myValueSize) {
        if (myValueSize < 1024) {
            log.info("对象大小： " + myValueSize + " B");
        } else if (Util.calKb(myValueSize) < 1024) {
            log.info("对象大小： " + getDecimalFormat().format(Util.calKb(myValueSize)) + " KB");
        } else if (Util.calKb(Util.calKb(myValueSize)) < 1024) {
            log.info("对象大小： " + getDecimalFormat().format(Util.calKb(Util.calKb(myValueSize))) + " MB");
        } else {
            log.info("对象大小： " + getDecimalFormat().format(Util.calKb(Util.calKb(Util.calKb(myValueSize)))) + " GB");
        }
    }

    /**
     * 显示上传进度
     *
     * @param transfer Transfer
     */
    public static void showTransferProgress(TransferProgress transfer) {

        boolean isDone = true;
        while (isDone) {
            try {
                // 每 1 秒获取一次进度
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            }
            TransferProgressSnapshot snapshot = transfer.snapshot();
            long sofar = snapshot.transferredBytes();
            long total = snapshot.totalBytes().isPresent() ? snapshot.totalBytes().getAsLong() : 0;
            double pct = snapshot.ratioTransferred().isPresent() ? snapshot.ratioTransferred().getAsDouble() : 0;
            System.out.printf("上传进度: [%d / %d] = %.02f%%\n", sofar, total, pct * 100);
            if (sofar >= total) {
                isDone = false;
            }
        }

        log.info("上传结果: {}", "Completed");
    }

    /**
     * 使用TransferManager上传大文件（异步线程）
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param filePath   本地文件路径
     */
    public static String multipartUploadWithTransferManager(String bucketName, String key, String filePath) {
        S3TransferManager transferManager = null;
        try {
            log.info("--------------------------使用TransferManager上传大文件（异步线程）开始: {} ---------------------------------", key);
            // 首先创建多部分上传并获取上传id
            log.info("在存储桶分片上传对象: {}", bucketName);
            log.info("分片上传对象 Key: {}", key);
            // 创建异步S3Client
            getS3AsyncClient();
            transferManager = S3TransferManager.builder()
                    .s3Client(s3AsyncClient)
                    .build();
            UploadFileRequest uploadFileRequest = UploadFileRequest.builder()
                    .putObjectRequest(b -> b
                            .bucket(bucketName)
                            .key(key))
                    .source(Paths.get(filePath))
                    .build();
            FileUpload fileUpload = transferManager.uploadFile(uploadFileRequest);
            showTransferProgress(fileUpload.progress());
            CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
            log.info("--------------------------异步执行TransferManager ---------------------------------");
            return uploadResult.response().eTag();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                closeAsync();
                if (transferManager != null) {
                    transferManager.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        return null;
    }

    /**
     * 使用S3Client上传大文件
     *
     * @param bucketName 桶名
     * @param key        对象在桶内的存储路径+文件名
     * @param filePath   本地文件路径
     * @return true表示上传成功，false表示上传失败
     */
    public static boolean multipartUploadWithS3Client(String bucketName, String key, String filePath) {
        RandomAccessFile file = null;
        getS3Client();
        try {
            log.info("--------------------------分片上传对象开始: {} ---------------------------------", key);
            // 首先创建多部分上传并获取上传id
            log.info("在存储桶分片上传对象: {}", bucketName);
            log.info("分片上传对象 Key: {}", key);
            // Initiate the multipart upload.
            CreateMultipartUploadResponse createMultipartUploadResponse = s3Client.createMultipartUpload(b -> b
                    .bucket(bucketName)
                    .key(key));
            String uploadId = createMultipartUploadResponse.uploadId();

            // Upload the parts of the file.
            int partNumber = 1;
            List<CompletedPart> completedParts = new ArrayList<>();
            // 5 MB byte buffer
            ByteBuffer bb = ByteBuffer.allocate(CHUNK_FILE_SIZE);

            file = new RandomAccessFile(filePath, "r");
            long fileSize = file.length();
            int position = 0;
            while (position < fileSize) {
                log.info("正在上传第：{} 个文件分片....", partNumber);
                file.seek(position);
                int read = file.getChannel().read(bb);
                // Swap position and limit before reading from the buffer.
                bb.flip();
                UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
                        .bucket(bucketName)
                        .key(key)
                        .uploadId(uploadId)
                        .partNumber(partNumber)
                        .build();
                UploadPartResponse partResponse = s3Client.uploadPart(
                        uploadPartRequest,
                        RequestBody.fromByteBuffer(bb));
                CompletedPart part = CompletedPart.builder()
                        .partNumber(partNumber)
                        .eTag(partResponse.eTag())
                        .build();
                completedParts.add(part);
                bb.clear();
                position += read;
                partNumber++;
            }
            // Complete the multipart upload.
            CompleteMultipartUploadResponse completeMultipartUploadResponse = s3Client.completeMultipartUpload(b -> b
                    .bucket(bucketName)
                    .key(key)
                    .uploadId(uploadId)
                    .multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build()));
            boolean isUploadSuccessful = completeMultipartUploadResponse.sdkHttpResponse().isSuccessful();
            log.info("isMultipartUploadSuccessful: {}", isUploadSuccessful);
            log.info("MultipartUploadObject statusCode: {}", completeMultipartUploadResponse.sdkHttpResponse().statusCode());
            log.info("MultipartUploadObject statusText: {}", completeMultipartUploadResponse.sdkHttpResponse().statusText());
            log.info("--------------------------上传对象: {} 完成---------------------------------", key);
            log.info("--------------------------分片上传对象结束: {} ---------------------------------", key);
            return isUploadSuccessful;
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                if (file != null) {
                    file.close();
                }
            } catch (IOException e) {
                log.error("Error occurred: ", e);
            }
        }
        return false;
    }

    /**
     * 把整个存储桶的对象下载到本地目录（异步线程）
     *
     * @param bucketName         桶名
     * @param destinationPathUri 本地目录路径
     * @return 下载失败的对象个数
     */
    public static int downloadObjectsToDirectory(String bucketName, String destinationPathUri) {
        S3TransferManager transferManager = null;
        try {
            // 创建异步S3Client
            getS3AsyncClient();
            transferManager = S3TransferManager.builder()
                    .s3Client(s3AsyncClient)
                    .build();
            DirectoryDownload directoryDownload = transferManager.downloadDirectory(
                    DownloadDirectoryRequest.builder()
                            .destination(Paths.get(destinationPathUri))
                            .bucket(bucketName)
                            .build());
            CompletedDirectoryDownload completedDirectoryDownload = directoryDownload.completionFuture().join();

            completedDirectoryDownload
                    .failedTransfers()
                    .forEach(fail -> log.warn("Object [{}] failed to transfer", fail.toString()));
            return completedDirectoryDownload.failedTransfers().size();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                closeAsync();
                if (transferManager != null) {
                    transferManager.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        return -1;
    }

    /**
     * 把本地目录上传到存储桶（异步线程）
     * 如果要上传A目录到存储桶且A目录名为key，则需要上传temp/A目录，否则直接上传A目录会把A目录下所有文件上传到存储桶的根目录
     *
     * @param bucketName      桶名
     * @param sourceDirectory 本地目录路径
     * @param tempDirectory   临时目录
     * @param uploadDirectory 上传目录
     * @return 上传失败的对象个数
     */
    public static int uploadDirectory(String bucketName, String sourceDirectory, String tempDirectory, String uploadDirectory) {
        S3TransferManager transferManager = null;
        try {
            // 创建异步S3Client
            getS3AsyncClient();
            transferManager = S3TransferManager.builder()
                    .s3Client(s3AsyncClient)
                    .build();
            File sourceDirectoryFile = new File(sourceDirectory);
            File tempUploadFile = new File(uploadDirectory);
            File uploadDirectoryFile = new File(tempUploadFile.getAbsolutePath() + "\\" + sourceDirectoryFile.getName());
            log.info("uploadDirectoryFile: {}", uploadDirectoryFile.getAbsolutePath());
            // 删除临时目录
            FileUtils.deleteDirectory(new File(tempDirectory));
            // 创建上传目录
            FileUtils.forceMkdir(uploadDirectoryFile);
            // 移动文件到上传目录
            Util.batchMoveFilesInFolders(sourceDirectory, uploadDirectoryFile.getAbsolutePath());
            DirectoryUpload directoryUpload = transferManager.uploadDirectory(UploadDirectoryRequest.builder()
                    .source(Paths.get(tempDirectory))
                    .bucket(bucketName)
                    .build());
            CompletedDirectoryUpload completedDirectoryUpload = directoryUpload.completionFuture().join();
            completedDirectoryUpload.failedTransfers()
                    .forEach(fail -> log.warn("Object [{}] failed to transfer", fail.toString()));
            return completedDirectoryUpload.failedTransfers().size();
        } catch (Exception e) {
            log.error("Error occurred: ", e);
        } finally {
            try {
                closeAsync();
                if (transferManager != null) {
                    transferManager.close();
                }
            } catch (Exception e) {
                log.error("Error occurred: ", e);
            }
        }
        return -1;
    }

    /**
     * 批量删除对象
     *
     * @param bucketName 桶名
     * @param keysList   对象key集合
     */
    public static void deleteBucketObjects(String bucketName, List<String> keysList) {
        getS3Client();
        log.info("--------------------------批量删除对象开始: {} ---------------------------------", keysList.size());
        ArrayList<ObjectIdentifier> keys = new ArrayList<>();
        ObjectIdentifier objectId;
        // 创建ObjectIdentifier对象
        for (String keyName : keysList) {
            //生成objectId
            objectId = ObjectIdentifier.builder()
                    .key(keyName)
                    .build();
            keys.add(objectId);
        }
        // 创建Delete对象
        Delete del = Delete.builder()
                .objects(keys)
                .build();
        try {
            // 创建DeleteObjectsRequest对象
            DeleteObjectsRequest multiObjectDeleteRequest = DeleteObjectsRequest.builder()
                    .bucket(bucketName)
                    .delete(del)
                    .build();
            s3Client.deleteObjects(multiObjectDeleteRequest);
            log.info("--------------------------批量删除对象: {} 完成---------------------------------", keysList.size());
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
    }

    /**
     * 获取对象版本
     *
     * @param bucketName 桶名
     * @param key        对象key
     * @return 对象版本集合
     */
    public static List<ObjectVersion> getObjectVersion(String bucketName, String key) {
        getS3Client();
        List<ObjectVersion> versionList = new ArrayList<>();
        try {
            ListObjectVersionsRequest listObjectVersionsRequest = ListObjectVersionsRequest.builder()
                    .bucket(bucketName)
                    .prefix(key)
                    .build();
            ListObjectVersionsResponse listObjectVersionsResponse = s3Client.listObjectVersions(listObjectVersionsRequest);
            return listObjectVersionsResponse.versions();
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
        return versionList;
    }

    /**
     * 获取桶是否开启对象版本
     *
     * @param bucketName 桶名
     */
    public static String getBucketVersioningStatus(String bucketName) {
        getS3Client();
        try {
            GetBucketVersioningRequest getBucketVersioningRequest = GetBucketVersioningRequest.builder()
                    .bucket(bucketName)
                    .build();
            GetBucketVersioningResponse bucketVersioningResponse = s3Client.getBucketVersioning(getBucketVersioningRequest);
            return bucketVersioningResponse.statusAsString();
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
        return null;
    }

    /**
     * 设置桶是否开启对象版本
     *
     * @param bucketName 桶名
     * @param status     状态
     */
    public static void setBucketVersioning(String bucketName, BucketVersioningStatus status) {
        getS3Client();
        try {
            PutBucketVersioningRequest putBucketVersioningRequest = PutBucketVersioningRequest.builder()
                    .bucket(bucketName)
                    .versioningConfiguration(VersioningConfiguration.builder()
                            .status(status)
                            .build())
                    .build();
            s3Client.putBucketVersioning(putBucketVersioningRequest);
        } catch (S3Exception e) {
            log.error("Error occurred: ", e);
        }
    }
}

